Skip to content

refactor(core): collapse activeElement & focusManager indirection#16

Merged
chiefcll merged 2 commits into
mainfrom
refactor/focus-cleanup
May 21, 2026
Merged

refactor(core): collapse activeElement & focusManager indirection#16
chiefcll merged 2 commits into
mainfrom
refactor/focus-cleanup

Conversation

@chiefcll
Copy link
Copy Markdown
Contributor

Summary

Two passes of cleanup against the src/core boundary, focused on the activeElement / focus-path machinery:

  1. Collapse Config.setActiveElement indirection. The solid signal for the active element used to live in src/activeElement.ts, with core/focusManager.ts notifying it via a settable Config.setActiveElement callback that the primitive useFocusManager re-wrapped to inject owner context. That's three layers for one signal write. The signal now lives in core/focusManager.ts, the Config.setActiveElement field is gone, and src/activeElement.ts is deleted.

  2. Merge the primitive useFocusManager into core. Now that core already imports solid-js (for the signal), the split into a core function + primitive wrapper was historical. The user-facing useFocusManager lives in core, captures its calling owner itself, and self-registers cleanup. focusPath is now a real solid signal — updateFocusPath updates it through the same owner-context wrapper used for key events, replacing the previous mirror effect the primitive maintained.

src/primitives/useFocusManager.ts is reduced to a 7-line named re-export so existing import paths (announcer, etc.) keep working untouched.

What changes for callers

  • Public surface unchanged: activeElement, setActiveElement, useFocusManager, focusPath, KeyMap, KeyHoldOptions are all still exported from the same import paths (@solidtv/solid and @solidtv/solid/primitives).
  • Config.setActiveElement is removed from the Config interface. If anything outside this repo was assigning to it, that assignment is now dead code and the assignment site can be deleted.

Why

One source of truth for active element and focus path. No callback re-binding, no two-layer useFocusManagerCore/useFocusManager, no separate signal that has to be kept in sync via an effect. The owner-context dance that the primitive used to perform is now four lines inside useFocusManager.

Test plan

  • npm run tsc — clean
  • npm test — 120/120 pass
  • npm run lint — 0 errors (pre-existing warnings only)
  • Manual smoke: focus navigation, programmatic .setFocus(), announcer focus-path subscription

🤖 Generated with Claude Code

chiefcll and others added 2 commits May 20, 2026 20:52
- Replace the mutable focusPath array with a real solid signal; updateFocusPath
  now sets it through the existing _signalWrapper, eliminating the mirror
  effect the primitive used to maintain.
- Collapse the two-layer useFocusManager (core + primitive) into a single
  function in core that captures its calling owner via getOwner/runWithOwner,
  attaches its own document listeners, and self-registers cleanup via onCleanup.
- Reduce primitives/useFocusManager.ts to a thin re-export shim so existing
  imports (announcer, etc.) keep working.

One source of truth for activeElement and focusPath, one useFocusManager,
no options-object/core-wrapper indirection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chiefcll chiefcll merged commit 4abef56 into main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant